﻿using Microsoft.AspNetCore.Http;
using Microsoft.Extensions.Logging;
using System;
using System.Collections.Generic;
using System.Diagnostics;
using System.IO;
using System.Linq;
using System.Text;
using System.Threading.Tasks;

namespace Micro.Extensions.Log
{
    public class TraceMiddleware
    {
        private readonly RequestDelegate _next;
        private readonly ILogger _logger;
        private static readonly IList<string> DefaultContents = new List<string>() {
         "image",
         "video",
         "audio",
         "excel",
         "world",
         "stream"
        };

        public TraceMiddleware(RequestDelegate next, ILogger<TraceMiddleware> logger)
        {
            _next = next;
            _logger = logger;
        }

        public async Task Invoke(HttpContext httpContext)
        {
            if (!_logger.IsEnabled(LogLevel.Information))
            {
                await _next(httpContext);
                return;
            }

            var stopWatch = new Stopwatch();
            stopWatch.Start();

            BuildHeaders(httpContext);

            var requestBodyString = await FormatRequestAsync(httpContext.Request);

            var originalBody = httpContext.Response.Body;

            using var currentBody = new MemoryStream();
            httpContext.Response.Body = currentBody;
            await _next(httpContext);
            httpContext.Response.Body.Seek(0, SeekOrigin.Begin);

            var (responseString, statusCode, contentLength) = await FormatResponseAsync(httpContext.Response);

            _logger.LogTrace(LogLevel.Information, new TraceMessage
            {
                Request = new Request
                {
                    Path = $"{httpContext.Request.Path}{httpContext.Request.QueryString.Value}",
                    Method = httpContext.Request.Method,
                    Body = requestBodyString?.Length > 4096 ? $"{requestBodyString.Substring(0, 4096)}..." : requestBodyString,
                    // ReSharper disable once PossiblyMistakenUseOfParamsMethod
                    Headers = httpContext.Request?.Headers?.ToDictionary(s => s.Key, s => string.Join(",", s.Value)),
                    ContentLength = string.IsNullOrEmpty(requestBodyString) ? 0 : requestBodyString.Length,
                },
                Response = new Response
                {
                    Body = responseString?.Length > 4096 ? $"{responseString.Substring(0, 4096)}..." : responseString,
                    ContentLength = contentLength,
                    StatusCode = statusCode,
                    ContentType = httpContext.Response.ContentType,
                    Headers = GetResponseHeader(httpContext.Response)
                },
                TimeSpan = (int)stopWatch.Elapsed.TotalMilliseconds
            });
            await currentBody.CopyToAsync(originalBody);
        }

        private Dictionary<string, string> GetResponseHeader(HttpResponse httpResponse)
        {
            var headers = new Dictionary<string, string>();

            foreach (var headerItem in httpResponse.Headers)
            {
                if ("Set-Cookie".Equals(headerItem.Key,
                            StringComparison.OrdinalIgnoreCase) ||
                    "Content-Type".Equals(headerItem.Key,
                            StringComparison.OrdinalIgnoreCase) ||
                    "Content-Length".Equals(headerItem.Key,
                            StringComparison.OrdinalIgnoreCase))
                {
                    continue;
                }

                headers[headerItem.Key] = headerItem.Value;
            }
            return headers;
        }

        private void BuildHeaders(HttpContext httpContext)
        {
            if (httpContext.Request.Headers.TryGetValue(LogConst.ChainId, out var chainId))
            {
                if (string.IsNullOrEmpty(chainId))
                    httpContext.Request.Headers.Add(LogConst.ChainId, Guid.NewGuid().ToString());
            }
            else
                httpContext.Request.Headers.Add(LogConst.ChainId, Guid.NewGuid().ToString());

            if (httpContext.Request.Headers.TryGetValue(LogConst.TrackId, out var trackId) && !string.IsNullOrEmpty(trackId))
            {
                httpContext.Request.Headers.Remove(LogConst.ParentTrackId);
                httpContext.Request.Headers.Add(LogConst.ParentTrackId, trackId);

                httpContext.Request.Headers.Remove(LogConst.TrackId);
                httpContext.Request.Headers.Add(LogConst.TrackId, Guid.NewGuid().ToString());
            }
            else
                httpContext.Request.Headers.Add(LogConst.TrackId, Guid.NewGuid().ToString());
        }

        private async Task<string> FormatRequestAsync(HttpRequest request)
        {
            try
            {
                if (request.ContentType != null && request.ContentType.ToLower().Contains("multipart/form-data"))
                    return "文件详情忽略";

                if (request.ContentLength.HasValue && request.ContentLength.Value > 0)
                {
                    request.EnableBuffering();

                    using StreamReader reader = new StreamReader(request.Body, Encoding.UTF8, false, 1024, true);
                    var requestBody = await reader.ReadToEndAsync();
                    request.Body.Position = 0;
                    return requestBody;
                }

                return "";
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "获取Request Body 错误");
                throw;
            }
        }

        private async Task<(string responseString, int statusCode, long contentLength)> FormatResponseAsync(HttpResponse response)
        {
            var isContainDefaultContentType = false;

            if (response.ContentType != null)
            {
                foreach (var item in DefaultContents)
                {
                    if (response.ContentType.Contains(item))
                    {
                        isContainDefaultContentType = true;
                        break;
                    }
                }
            }

            if (isContainDefaultContentType)
            {
                return ("检测到文件流，内容不记录", response.StatusCode, response.ContentLength ?? 0);
            }

            try
            {
                var responseString = string.Empty;

                var length = 4096;
                if (response.Body.Length < length)
                    length = (int)response.Body.Length;

                var readBuffer = new char[length];
                var reader = new StreamReader(response.Body);
                var readLength = await reader.ReadAsync(readBuffer, 0, length);

                if (readLength > 0)
                    responseString = new string(readBuffer, 0, readLength);

                response.Body.Seek(0, SeekOrigin.Begin);
                return (responseString, response.StatusCode, response.ContentLength ?? 0);
            }
            catch (Exception ex)
            {
                _logger.LogError(ex, "获取Response Body 错误");
                throw;
            }
        }
    }
}
